/*
 Copyright (c) 2009, Ivan Popivanov
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions
 are met:

 * Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.

 * Neither the name of StockCandles nor the names of its contributors
 may be used to endorse or promote products derived from this software
 without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <QtCore/QFile>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QTextStream>

#include "ta_libc.h"

#include "CandleSeries.h"
#include "CsvFile.h"

CandleSeries::CandleSeries()
{
}

CandleSeries::CandleSeries(const QString &symbol)
{
   this->symbol = symbol;
}

CandleSeries::~CandleSeries()
{
}

int
CandleSeries::findDateIndex(const QDateTime &dateTime)
{
   for(int i = 0; i < size(); ++i)
   {
      if(dateTime.date() == at(i).getDateTime().date()) return i;
   }

   return -1;
}

bool
CandleSeries::loadFromYahooCsv(const QString &path, const QDateTime *start /* = NULL */)
{
   bool result = false;

   resize(0);

   CsvFile file(path);

   int tParsing = 0;
   int tSplit = 0;

   // Time the operation
   QTime t;
   t.start();
   if(file.open(QFile::ReadOnly))
   {
      // QTextStream in(&file);

      QStringList list;

      // Skip the first line
      if(!file.readLine(list)) return false;

      while(file.readLine(list))
      {
         QTime tt; tt.start();
         tSplit += tt.elapsed();

         if(list.size() != 7) {resize(0); return false;}

         tt.restart();
         bool ok = false;

         double open = list.at(1).toDouble(&ok); if(!ok) {resize(0); return false;}
         double high = list.at(2).toDouble(&ok); if(!ok) {resize(0); return false;}
         double low = list.at(3).toDouble(&ok); if(!ok) {resize(0); return false;}
         double close = list.at(4).toDouble(&ok); if(!ok) {resize(0); return false;}
         double volume = list.at(5).toDouble(&ok); if(!ok) {resize(0); return false;}
         double adjClose = list.at(6).toDouble(&ok); if(!ok) {resize(0); return false;}

         QDateTime dt = QDateTime::fromString(list.at(0), "yyyy-MM-dd");
         tParsing += tt.elapsed();

         if(start != NULL && *start > dt) break;

         if (close != adjClose)
         {
            double d = adjClose / close;
            open = round(d * open);
            high = round(d * high);
            low = round(d * low);
            close = round(adjClose);
            volume = round(d * volume);
         }

         push_back(Candle(dt, open, high, low, close, (uint)volume));
      }

      result = true;
   }
   else
   {
      result = false;
   }

   return result;
}

bool
CandleSeries::storeToYahooCsv(const QString &path)
{
   CsvFile file(path);

   if(file.open(QFile::WriteOnly | QFile::Truncate))
   {
      // output the header
      if(!file.writeLine("Date,Open,High,Low,Close,Volume,Adj Close")) return false;

      for(int i = 0; i < size(); ++i)
      {
         const Candle &c = at(i);
         QStringList fields;
         fields.append(c.getDateTime().date().toString("yyyy-MM-dd"));
         fields.append(QString::number(c.getOpen(), 'f', 2));
         fields.append(QString::number(c.getHigh(), 'f', 2));
         fields.append(QString::number(c.getLow(), 'f', 2));
         fields.append(QString::number(c.getClose(), 'f', 2));
         fields.append(QString::number(c.getVolume()));
         fields.append(QString::number(c.getClose(), 'f', 2));
         if(!file.writeLine(fields)) return false;
      }
      file.close();
   }
   else
   {
      return false;
   }
   return true;
}

CandleSeries
CandleSeries::compress(uint candlesPerCandle, int maxLen /* = -1 */)
{
   CandleSeries result;

   CandleSeries::const_iterator iter = begin();
   uint candlesCombined = 0;
   while(iter != end())
   {
      if(candlesCombined == 0)
      {
         if(maxLen == result.size())
         {
            break;
         }
         result.push_back(*iter);
      }
      else
      {
         uint i = result.size() - 1;
         if(iter->getHigh() > result.at(i).getHigh()) result[i].setHigh(iter->getLow());
         if(iter->getLow() < result.at(i).getLow()) result[i].setLow(iter->getLow());

         result[i].setClose(iter->getClose());
      }

      candlesCombined = (candlesCombined + 1) % candlesPerCandle;
      ++iter;
   }

   return result;
}

CandleSeries
CandleSeries::dailyTo(char resolution, int numUnits /* = -1 */)
{
   // Currently the output is always of the same length as the
   // original series. Keep in mind if this function is merged
   // with the series "compression" code (see compress).
   CandleSeries result;
   result.reserve(size());

   for(int i = 0; i < size(); ++i)
   {
      // Copy the candle
      Candle ci = at(i);

      for(int j=i+1; j<size(); ++j)
      {
         const Candle &cj = at(j);

         // Check whether we have gone too far
         bool tooFar = false;
         if(resolution == 'w')
         {
            if(ci.getDateTime().date().daysTo(cj.getDateTime().date()) <= -7)
            {
               tooFar = true;
            }
         }
         else if(resolution == 'm')
         {
            if(ci.getDateTime().date().addMonths(-1) >= cj.getDateTime().date())
            {
               tooFar = true;
            }
         }
         else if(resolution == 'q')
         {
            if(ci.getDateTime().date().addMonths(-3) >= cj.getDateTime().date())
            {
               tooFar = true;
            }
         }
         else if(resolution == 'd')
         {
            if( numUnits < (j - i))
            {
               tooFar = true;
            }
         }

         if(tooFar)
         {
            // Summarize the volume
            int days = j - i;
            if(days > 0)
            {
               ci.setVolume(ci.getVolume() / days);
            }
            break;
         }

         ci.setOpen(cj.getOpen());
         ci.setVolume(ci.getVolume() + cj.getVolume());

         if(cj.getHigh() > ci.getHigh())
         {
            ci.setHigh(cj.getHigh());
         }

         if(cj.getLow() < ci.getLow())
         {
            ci.setLow(cj.getLow());
         }
      }

      result.push_back(ci);
   }

   return result;
}

// The results are reversed - the oldest value has index 0
bool
CandleSeries::getRawData(double *&opens, double *&highs, double *&lows, double *&closes, uint *&volumes)
{
   return size() == getRawSample(opens, highs, lows, closes, volumes, 0, 'd', 1, size());
}

int
CandleSeries::getRawSample(double *&opens, double *&highs, double *&lows, double *&closes, uint *&volumes,
                           int first, int resolution, int units, int maxCnt)
{
   int result = -1;
   if(opens == NULL)
   {
      opens = new double [size()];
      if(opens == NULL) { goto error; }
   }

   if(highs == NULL)
   {
      highs = new double [size()];
      if(highs == NULL) { goto error; }
   }

   if(lows == NULL)
   {
      lows = new double [size()];
      if(lows == NULL) { goto error; }
   }

   if(closes == NULL)
   {
      closes = new double [size()];
      if(closes == NULL) { goto error; }
   }

   if(volumes == NULL)
   {
      volumes = new uint [size()];
      if(volumes == NULL) { result = false; goto error; }
   }

   int i = first;
   int j = 0;
   int outIdx = maxCnt - 1;
   while(i < size() && j < maxCnt)
   {
      const Candle &c = at(i);

      opens[outIdx] = c.getOpen();
      highs[outIdx] = c.getHigh();
      lows[outIdx] = c.getLow();
      closes[outIdx] = c.getClose();
      volumes[outIdx] = c.getVolume();

      bool done = false;
      if(resolution == 'd')
      {
         i += units;
      }
      else if(resolution == 'w')
      {
         // Pick a good starting point - a sequential lookup is too slow.
         i += 5*units;
         QDate stopDate = c.getDateTime().date().addDays(-7*units);

         // Exit if we exceeded size - rarely we can miss a week (if it's
         // shorter than five work days), but that's not a big deal
         if(i >= size())
         {
            done = true;
         }
         else
         {
            // We may need to move closer to stopDate in case we skipped days
            // with less than five work days.
            while(at(i-1).getDateTime().date() <= stopDate)
            {
               --i;
            }
         }
      }
      else if(resolution == 'm')
      {
         // Pick a good starting point - there are less than 24 work day per month
         //    4 * (5 + 2) = 28 + 4 = 32, thus 4 * 5 + 4 = 24 is the maximum of
         //    work days
         //
         // Again we may miss a month at the end but it's not worth checking the
         // exact end
         i += 24*units;
         QDate stopDate = c.getDateTime().date().addMonths(-units);

         if(i >= size())
         {
            done = true;
         }
         else
         {
            // We may need to move closer to stopDate in case we skipped days
            while(at(i-1).getDateTime().date() <= stopDate)
            {
               --i;
            }
         }
      }
      else if(resolution == 'q')
      {
         i += 72*units;
         QDate stopDate = c.getDateTime().date().addMonths(-3*units);

         if(i >= size())
         {
            done = true;
         }
         else
         {
            // We may need to move closer to stopDate
            while(at(i-1).getDateTime().date() <= stopDate)
            {
               --i;
            }
         }
      }

      ++j;
      --outIdx;

      if(done) break;
   }

   result = j;

exit:
   return result;

error:
   if(opens != NULL) delete [] opens;
   if(highs != NULL) delete [] highs;
   if(lows != NULL) delete [] lows;
   if(closes != NULL) delete [] closes;
   if(volumes != NULL) delete [] volumes;
   goto exit;
}